No es novedad que las redes sociales desde la primera década de este siglo han cobrado gran importancia en la vidas de las personas, interactuando con ellas de forma cotidiana. Por lo tanto, los análisis y la utilidad que se pueden sacar a ellas pueden llegar a ser de enorme importancia para empresas, instituciones públicas, partidos políticos, etc.
En el presente trabajo se aborda una problemática latente de varios países en el mundo, como lo son los ataques terroristas. Específicamente se busca, utilizando datos generados a través de Twitter, hacer un estudio de cuatro grandes atentados que afectaron a Inglaterra durante el año 2017; específicamente se estudiarán los atentados de Finsbury Park (19 de junio), London Bridge (3 de junio), Manchester Arena (22 de mayo) y Westminster (22 de mayo), los cuales a priori están relacionados a ataques islamófobos o yihadistas. Estos atentados se inscriben en una serie de ataques en Europa que empezó en 2015, impulsado por el Estado Islámico.
En este proyecto, en particular, se busca:
Los modelos y los resultados son expuesto a continuación en este trabajo.
La base de datos a trabajar para este hito fueron los ataque terroristas ,en Inglaterra ocurridos durante el 2017, estos corresponden al registro de mas de 60000 tweets por cada ataque registrados bajo la búsqueda de 4 keywords relacionadas al ataque, por ejemplo para el caso de finsbury se tiene "terrorist", "attack", "finsbury", "london". Dicho esto los atributos utilizados para caracterizar los tweets son los siguientes:
Es necesario recalcar el hecho de que el atributo class fue creado de manera manual por los integrantes del grupo, pero para esta iteracion del proyecto solo se consideraron 100 tweets por hora lo cual es equivalente a 2400 tweets por cada atentado.
Antes de proceder con el analisis y clasificación fue necesario limpiar nuestro dataset lo cual consistió en dos principales etapas.
En esta parte del trabajo, se muestra el código utilizado para poder levantar información sobre la evolución de los sentimientos en los tweets publicados durente un horizonte temporal anterior y posterior al atentado terrorista ocurrido en Finsbury Park el año 2017. Como se mencionó anteriormente, se trabaja solo con esta base de datos debido a que se requiere hacer una clasificación manual de los datos y, a modo de ejemplificar el uso de las diversas herramientas aprendidas, basta para este hito la utilización de la base de datos de un solo atentado terrorista.
# unzip if necesary
# !unzip data.zip
#Test lbsa lib
# https://github.com/AntoinePassemiers/Lexicon-Based-Sentiment-Analysis
#Importar librerías
import numpy as np
import pandas as pd
import lbsa
import matplotlib.pyplot as plt
import seaborn as sn
import codecs
import string
import time
from calendar import timegm
#from google.colab import files
from scipy.stats import t
%matplotlib inline
A continuación se muestro un ejemplo de la utilización de la libreria lbsa (lexicon based sentiment analysis), en conjunto con lexicons de NRC, con la finalidad de econtrar un vector de caracteristicas compuesto por :
['anger' , 'anticipation', 'disgust', 'fear', 'joy', 'sadness', 'surprise' 'trust', 'positive', 'negative' ]
sa_lexicon = lbsa.get_lexicon('sa', language='english', source='nrc')
op_lexicon = lbsa.get_lexicon('opinion', language='english', source='nrc')
tweet = """
The Budget Agreement today is so important for our great Military.
It ends the dangerous sequester and gives Secretary Mattis what he needs to keep America Great.
Republicans and Democrats must support our troops and support this Bill!
"""
extractor = lbsa.FeatureExtractor(sa_lexicon, op_lexicon)
extractor.process(tweet)
Con lo anterior sumado a nuestro dataset con los tweets, se utilizó un pequeño script que genera 24 archivos de texto que contienen los tweets en un intervalo de 1 hora, luego se aplicó el análisis de sentimiento presentado anteriormente sobre estos bloques de tweets con la finalidad de estudiar el comportamiento durantes las 12 horas previas y las 12 horas posteriores al atentado.
Cabe destacar que para poder evaluar si las diferencias son significativas para el caso de los sentimientos de "miedo" e "ira", se realizó un test de diferencias de medias. La distribución del estimador para este caso fue una t de Student debido a que el tamaño de la muestra es menor a 30 (24) para haber escogido una normal.
A continuación son mostrados los gráficos juntos a los p-valores para los sentimientos en cuestión. Cabe destacar que el p-valor corresponde que la hipótesis nula (en este caso, que el promedio de los sentimientos son iguales antes y después del atentado) sea cierta y, en este caso, toleraremos un error del 5% o 0,05.
def dependent_ttest(data1, data2, alpha):
# calculate means
mean1, mean2 = np.mean(data1), np.mean(data2)
# number of paired samples
n = len(data1)
# sum squared difference between observations
d1 = sum([(data1[i]-data2[i])**2 for i in range(n)])
# sum difference between observations
d2 = sum([data1[i]-data2[i] for i in range(n)])
# standard deviation of the difference between means
sd = np.sqrt((d1 - (d2**2 / n)) / (n - 1))
# standard error of the difference between the means
sed = sd / np.sqrt(n)
# calculate the t statistic
t_stat = (mean1 - mean2) / sed
# degrees of freedom
df = n - 1
# calculate the critical value
cv = t.ppf(1.0 - alpha, df)
# calculate the p-value
p = (1.0 - t.cdf(abs(t_stat), df)) * 2.0
# return everything
return p
Esta función sirve para graficar temporalmente la evolución de los grados de las emociones de los tweets, además se le aplica el test de hipótesis anteriomente nombrado.
def get_sentiment(src_data, place):
lexicon = lbsa.get_lexicon('sa', language='english')
op_lexicon = lbsa.get_lexicon('opinion', language='english', source='nrc')
tag_names = lexicon.get_tag_names()
dict_feat = {'anger':[], 'anticipation':[], 'disgust':[], 'fear':[], 'joy':[], 'sadness':[], 'surprise':[], 'trust':[]}
len_text_array = []
for i in range(24):
try:
f = open(src_data + str(i) +".txt", 'r',encoding='utf-8')
text = f.read()
text = text.encode("ascii", errors="ignore").decode()
# El largo de los textos va a servir para normalizar
len_text = len(text.split())
len_text_array.append(len_text)
extractor = lbsa.FeatureExtractor(lexicon, op_lexicon)
tmp_feat = (lexicon.process(text))
for feature_name in tag_names:
dict_feat[feature_name].append(tmp_feat[feature_name])
except:
break
#print(dict_feat)
x_label = np.subtract(range(24), 12)
for feature_name in tag_names:
feature = (dict_feat[feature_name])
#print(feature)
plt.plot(x_label, feature, label=feature_name)
plt.title(place)
plt.legend()
plt.xlabel("Hora relativa")
plt.ylabel("Presencia sentimiento")
plt.show()
feature = (np.divide(dict_feat['fear'],len_text_array))
p1 = feature[range(0,12)]
p2 = feature[range(12,24)]
print('Miedo: p-valor:', dependent_ttest(p1,p2,0.05))
feature = (np.divide(dict_feat['anger'],len_text_array))
p1 = feature[range(0,12)]
p2 = feature[range(12,24)]
print('Rabia: p-valor:', dependent_ttest(p1,p2,0.05))
fins_path = "data/fins/"
get_sentiment(fins_path, "finsbury")
fins_path = "data/man/"
get_sentiment(fins_path, "manchester")
fins_path = "data/london/"
get_sentiment(fins_path, "london")
fins_path = "data/west/"
get_sentiment(fins_path, "westminster")
Esta vez los dataset contienen solo 100 tweets por hora para cada atentado, por lo cual no es necesario normalizar.
Se desprende del análisis de sentimientos lo siguiente:
En el hito anterior se utilizaron cuatro modelos de clasificación, los cuales son: redes neuronales, support vector machine, árboles de decisión y naive bayes. Los cuales fueron ejecutadas y evaluados a través de métricas como el accuracy, precision y recall. De ellos se obtuvieron las siguiente conclusiones:
De los resultados de las métricas, se puede mencionar que:
Considerando esto los experimentos realizados para este último hito solo consideraron los modelos que entregaron mejores resultados, redes neuronales y SVN.
A continuación se presentan los resultdos que se obtuvieron al utilizar dichos clasificadores sobre el nuevo dataset híbrido que incluye los cuatro atentados.
filecp = codecs.open("data/merge.tsv", encoding = 'utf8')
data = pd.read_csv(filecp, sep="\t", encoding="utf-8", )
data.reset_index(drop=True)
# Creamos un arreglo vacío en el que se agregaran los vectores con los atributos
# usados durante el entrenamiento para cada fila
data_array = []
# Obtenemos la cantidad de filas
rows = data["class"].count()
print("Total filas: ", rows)
src_text_dict = {
"Twitter for iPhone":0,
"Twitter Web Client":1,
"Twitter for Android":2
}
sa_lexicon = lbsa.get_lexicon('sa', language='english', source='nrc')
op_lexicon = lbsa.get_lexicon('opinion', language='english', source='nrc')
# Guardamos un txt con todos los tweets,a brimos el archivo en modo "append"
# Limpiamos el txt
open("text_tweets.txt", 'w').close()
f = open("text_tweets.txt", "a")
for i in range(rows):
rt_count = data.loc[i,"rt_count"] if data.loc[i,"rt_count"] else 0
followers_count = data.loc[i,"followers_count"] if data.loc[i,"followers_count"] else 0
#epoch_time = timegm(time.strptime(data.loc[i,"creation_date"], "%Y-%m-%d %H:%M:%S")) if data.loc[i,"creation_date"] else 0
tmp_row = [rt_count, followers_count]
src = data.loc[i,"src_text"]
txt_src = src_text_dict[src] if src in src_text_dict else 3
tmp_row.append(txt_src)
# Calculamos el vector generado por el analisis de sentimiento
tweet = data.loc[i,"text_tweet"]
f.write(tweet + "\n")
extractor = lbsa.FeatureExtractor(sa_lexicon, op_lexicon)
#print(i, tweet)
sentimental = extractor.process(tweet)
tmp_row.extend(sentimental)
# Finalmente agregamos la clase
tmp_row.append(data.loc[i,"class"])
data_array.append(tmp_row)
# Guardamos el archivo separado por tabs
np.savetxt("data_train_test.tsv", data_array, delimiter="\t")
f.close()
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import classification_report, confusion_matrix
names = ['rt_count', 'followers_count', 'src_tweet', 'anger', 'anticipation',
'disgust', 'fear', 'joy', 'sadness', 'surprise', 'trust', 'positive',
'negative', 'class']
data = pd.read_csv("data_train_test.tsv", sep="\t", names=names)
# Atributos
X = data.iloc[:, 0:len(data.columns)-2]
# Clases
y = data.iloc[:, len(data.columns)-1]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20)
# Normalize, standarize
scaler = StandardScaler()
scaler.fit(X_train)
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)
mlp = MLPClassifier(hidden_layer_sizes=(10, 10), max_iter=1000)
mlp.fit(X_train, y_train.values.ravel())
predictions = mlp.predict(X_test)
con_mat = confusion_matrix(y_test,predictions)
#print("Matriz de confusión \n", con_mat, "\n")
labels = ["True", "False"]
ax = plt.axes()
df_cm = pd.DataFrame(con_mat, index = [i for i in labels], columns = [i for i in labels], dtype='int32')
sn.heatmap(df_cm, annot=True, fmt='g')
ax.set_title('Matriz de confusión: Red Neuronal')
print("Accuracy Total: ", (con_mat[0][0]+con_mat[1][1])/sum(sum(con_mat)))
print(classification_report(y_test,predictions))
plt.savefig("sentiment_nn.png", dpi=300)
from sklearn.svm import SVC # support vector machine classifier
svm = SVC()
svm.fit(X_train, y_train)
predictions_3 = svm.predict(X_test)
con_mat_3 = confusion_matrix(y_test,predictions_3)
#print("Matriz de confusión \n", con_mat_3, "\n")
labels = ["True", "False"]
ax = plt.axes()
df_cm = pd.DataFrame(con_mat_3, index = [i for i in labels], columns = [i for i in labels], dtype='int32')
sn.heatmap(df_cm, annot=True, fmt='g')
ax.set_title('Matriz de confusión: SVN')
print("Accuracy Total: ", (con_mat_3[0][0]+con_mat_3[1][1])/sum(sum(con_mat_3)))
print(classification_report(y_test,predictions_3))
plt.savefig("sentiment_svn.png", dpi=300)
Los nuevos resultados entregan aproximadamente un 10% menos de precisión que los obtenidos en hito II donde solo se utilizó un dataset, por lo cual es posible notar que un modelo basado solo en análisis de sentimientos no es suficiente apra resulver este problema.
Al momento de entrenar los modelos anteriormente expuestos para la nueva data, se pudo evidenciar una baja precisión (accuracy), lo cual se puede deber al hecho que emociones como la ira o el miedo siempre predominan, que sea antes o después de un ataque y lo cual hace que le quite explicación a la variabilidad a la data como para poder hacer una buena clasificación con esta información. Intentamos agregar otros atributos para clasificar como la fecha emisión del tweet pero los resultados no cambiaron. Por lo tanto, otro enfoque que se tomó para poder utilizar la información de los tweets y mejorar los algoritmos de clasificación fue el uso de "bag of words".
Se implementó un nuevo modelo de clasificación utilizando TF-IDF o "Term frequency – Inverse document frequency" que traducido al español es "Frecuencia de términos – Frecuencia inversa del documento" el cual otorga una medida numérica que expresa cuán relevante es una palabra para un documento en una colección de estos.
El valor TF-IDF aumenta proporcionalmente al número de veces que una palabra aparece en el documento, pero es compensada por la frecuencia de la palabra en la colección de documentos.
Utilizando la libreria sklearn se generó un vector de 1500 caracteristicas usando TF-IDF, para esto fue necesario pre procesar lso tweets nuevamente, elimando stop words y "lemmatizando" para poder usar el método de Bag of Words apra clasificar.
# Vector de caracteristicas sentimeintos + tfidf, se almacena en un archivo diferente
from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from nltk.stem import WordNetLemmatizer
import nltk
nltk.download('wordnet')
stemmer = WordNetLemmatizer()
filecp = codecs.open("data/merge.tsv", encoding = 'utf8')
data = pd.read_csv(filecp, sep="\t", encoding="utf-8", )
data.reset_index(drop=True)
# Creamos un arreglo vacío en el que se agregaran los vectores con los atributos
# usados durante el entrenamiento para cada fila
tweet_array = []
# Obtenemos la cantidad de filas
rows = data["class"].count()
for i in range(rows):
tweet = data.loc[i,"text_tweet"]
# Limpiamos el tweet
tweet = [stemmer.lemmatize(word) for word in tweet]
tweet = ''.join(tweet)
tweet_array.append(tweet)
vectorizer = CountVectorizer(max_features=1500, min_df=5, max_df=0.7, stop_words='english')
X = vectorizer.fit_transform(tweet_array).toarray()
tfidfconverter = TfidfTransformer()
feature_vect = tfidfconverter.fit_transform(X).toarray()
# Creamos un arreglo vacío en el que se agregaran los vectores con los atributos
# usados durante el entrenamiento para cada fila
data_array = []
print("Total filas: ", rows)
src_text_dict = {
"Twitter for iPhone":0,
"Twitter Web Client":1,
"Twitter for Android":2
}
sa_lexicon = lbsa.get_lexicon('sa', language='english', source='nrc')
op_lexicon = lbsa.get_lexicon('opinion', language='english', source='nrc')
for i in range(rows):
rt_count = data.loc[i,"rt_count"] if data.loc[i,"rt_count"] else 0
followers_count = data.loc[i,"followers_count"] if data.loc[i,"followers_count"] else 0
tmp_row = [rt_count, followers_count]
src = data.loc[i,"src_text"]
txt_src = src_text_dict[src] if src in src_text_dict else 3
tmp_row.append(txt_src)
# Calculamos el vector generado por el analisis de sentimiento
tweet = data.loc[i,"text_tweet"]
extractor = lbsa.FeatureExtractor(sa_lexicon, op_lexicon)
sentimental = extractor.process(tweet)
tmp_row.extend(sentimental)
tmp_row.extend(feature_vect[i])
# Finalmente agregamos la clase
tmp_row.append(data.loc[i,"class"])
data_array.append(tmp_row)
# Guardamos el archivo separado por tabs
np.savetxt("data_train_test2.tsv", data_array, delimiter="\t")
print(len(data_array))
# Test net, solo utiliza el vector generado por tfidf
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import classification_report, confusion_matrix
data = pd.read_csv("data_train_test2.tsv", sep="\t")
# Atributos
X = data.iloc[:, 0:len(data.columns)-2]
# Si queremos solo el vector generado por tfidf (1500 caracteristicas)
X = data.iloc[:, len(data.columns)-1502:len(data.columns)-2]
# Clases, se encuentran en la ultima columna
y = data.iloc[:, len(data.columns)-1]
print(len(feature_vect))
print(len(y))
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20)
# Normalize, standarize
scaler = StandardScaler()
scaler.fit(X_train)
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)
mlp = MLPClassifier(hidden_layer_sizes=(250, 250), max_iter=2500, verbose=False)
mlp.fit(X_train, y_train.values.ravel())
predictions = mlp.predict(X_test)
con_mat = confusion_matrix(y_test,predictions)
#print("Matriz de confusión \n", con_mat, "\n")
labels = ["True", "False"]
ax = plt.axes()
df_cm = pd.DataFrame(con_mat, index = [i for i in labels], columns = [i for i in labels], dtype='int32')
sn.heatmap(df_cm, annot=True, fmt='g')
ax.set_title('Matriz de confusión: Red Neuronal')
print("Accuracy Total: ", (con_mat[0][0]+con_mat[1][1])/sum(sum(con_mat)))
print(classification_report(y_test,predictions))
#plt.savefig("tfidf_nn.png", dpi=300)
Los resultados obtenidos son muy superiores a los observados utilizando solo análisis de sentimientos, más aún considerando que estos resultados pueden mejorar modificando lso aprametros de la red y el vector de características.
Conclusiones finales
Del trabajo semestral realizado se desprenden las siguientes conclusiones: